package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket
        运行在服务端的ServerSocket主要有两个作用:
        1:向系统申请服务端口
        2:监听该端口，一旦一个客户端通过该端口连接就立即返回一个Socket
          通过这个Socket与客户端对等通讯。
        如果我们将Socket比喻为电话，那么ServerSocket相当于总机。
     */
    private ServerSocket serverSocket;
    /*
        保存所有客户端的输出流，以便ClientHandler之间广播消息
     */
//    private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut = new ArrayList<>();

    public Server(){
        try {
            /*
                创建ServerSocket时需要指定服务端口，客户端就是通过这个端口与服务端建立
                连接的。该端口不能与操作系统上其它应用程序一致，否则会抛出异常:
                java.net.BindException:address already in use
                解决办法:更换端口，直到不冲突
             */
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();//相当于"接电话"的动作
                System.out.println("一个客户端连接了!");
                //启动一个线程与该客户端交互
                ClientHandler handler = new ClientHandler(socket);//把客户端的socket连接传给线程
                Thread t = new Thread(handler);
                t.start();

            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    private class ClientHandler implements Runnable{
        private Socket socket;
        private String host;//记录该客户端的IP地址信息

        public ClientHandler(Socket socket){
            this.socket = socket;
            //通过socket获取远端计算机的地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            PrintWriter pw = null;
            try {
                /*
                    通过Socket提供的方法:getInputStream()获取的字节输入流可以读取远端计算机
                    发送过来的字节数据。
                 */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                //通过socket获取输出流，用于将消息发送给客户端
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);
                //将该输出流存入共享数组allOut中
                /*
                    在使用同步块时，指定同步监视器对象时，通常指定临界资源即可。即:
                    多个线程抢着操作的该对象。但是需要注意，这个对象不得在同步块中
                    被更换!
                 */
                //这里不能使用allOut是因为它虽然是临界资源，但是同步块中扩容时会更换该对象
//                synchronized (allOut) {
//                synchronized (Server.this){

                //若allOut是一个集合，增删元素并不会创建新集合对象，因此可直接作为锁对象
                synchronized (allOut){
                    //1对数组扩容
//                    allOut = Arrays.copyOf(allOut, allOut.length + 1);
                    //2将输出流存入数组最后一个位置(刚扩出来的格子)
//                    allOut[allOut.length - 1] = pw;

                    allOut.add(pw);
                }

//                sendMessage(host+"上线了，当前在线人数:"+allOut.length);
                sendMessage(host+"上线了，当前在线人数:"+allOut.size());

                String message = null;
                /*
                    读取客户端发送过来的一行字符串。这里调用后该方法会阻塞，直到客户端真实发送了
                    一行字符串才会返回。
                    当客户端断开连接时，由于客户端断开方式不同，这里readLine方法反映也不同
                    当客户端异常断开(没有执行socket.close())时，readLine会抛出异常。
                    当客户端正常断开时，readLine方法会返回null表示流读取到末尾了。
                 */
                while((message = br.readLine()) != null){
                    System.out.println(host+"说:"+message);
                    //遍历allOut中的所有输出流将消息发送给所有客户端
                    sendMessage(host + "说:" + message);
                }
            } catch (IOException e) {
//                e.printStackTrace();
            } finally {
                //处理该客户端断开连接后的操作
                synchronized (allOut) {
                    //将当前客户端的输出流从数组allOut中删除
//                    for (int i = 0; i < allOut.length; i++) {
//                        if (allOut[i] == pw) {
//                            allOut[i] = allOut[allOut.length - 1];
//                            allOut = Arrays.copyOf(allOut, allOut.length - 1);
//                            break;
//                        }
//                    }
                    allOut.remove(pw);
                }
//                sendMessage(host+"下线了，当前在线人数:"+allOut.length);
                sendMessage(host+"下线了，当前在线人数:"+allOut.size());
                try {
                    //关闭socket，释放资源
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void sendMessage(String message){
            synchronized (allOut) {
                for (PrintWriter pw : allOut) {
                    pw.println(message);
                }
            }
        }

    }

}
